﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using MugenInjection.Attributes;
using MugenInjection.Delegates;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Interface.Components;

// ReSharper disable CheckNamespace
namespace MugenInjection.Core.Components
// ReSharper restore CheckNamespace
{
    /// <summary>
    /// The base class for components for the automatically finds and loads modules from assemblies.
    /// </summary>
    public class ModuleManagerComponent : Component, IModuleManagerComponent
    {
        #region Fields

        protected static readonly object StaticLocker = new object();
        protected static Dictionary<Assembly, IList<Type>> CachedAssembly;
        private readonly List<Type> _loadedModule = new List<Type>();

#if SCANASSEMBLY
        private static bool _isFirstScanAssembly;
#endif
#if !NOLOADASSEMBLY
        private static bool _isFirstLoadAssembly;
#endif

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ModuleManagerComponent"/> class.
        /// </summary>
        static ModuleManagerComponent()
        {
            CachedAssembly = new Dictionary<Assembly, IList<Type>>();
        }

#if SCANASSEMBLY
        /// <summary>
        /// Initializes a new instance of the <see cref="ModuleManagerComponent" /> class.
        /// </summary>
        public ModuleManagerComponent()
            : this(new List<string> { "*.exe", "*.dll" })
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ModuleManagerComponent" /> class.
        /// </summary>
        public ModuleManagerComponent(IList<string> extensions)
        {
            Validate.ArgumentNotNull(extensions, "extensions");
            ExcludedFiles = new List<string>();
            Extensions = extensions;
        }
#endif

        #endregion

        #region Methods

        /// <summary>
        /// Loads modules defined in the specified assemblies.
        /// </summary>
        /// <param name="assemblies">The assemblies to search.</param>
        public virtual void LoadModules(params Assembly[] assemblies)
        {
            Validate.ArgumentNotNullAndNonZeroLength(assemblies, "assemblies");
            LoadModulesInternal(assemblies);
        }

#if SCANASSEMBLY
        /// <summary>
        /// Indicates whether the automatic scan of the files, default is <c>true</c>.
        /// </summary>
        public bool AutoScanAssembly { get; set; }

        /// <summary>
        /// Gets the extensions for load. The default extensions is .exe and .dll.
        /// </summary>
        public IList<string> Extensions { get; private set; }

        /// <summary>
        /// Gets the excluded files.
        /// </summary>
        public IList<string> ExcludedFiles { get; private set; }

        /// <summary>
        /// Rescans files.
        /// </summary>
        public virtual void RescanFiles()
        {
            lock (StaticLocker)
            {
                string directory = AppDomain.CurrentDomain.BaseDirectory;
                foreach (string extension in Extensions)
                {

                    foreach (var assembly in GetAssembliesInDirectoryWithExtension(directory, extension,
                                                                          Injector.Settings.ThrowErrorIfNotLoadAssembly,
                                                                          ExcludedFiles))
                        ExportTypes(assembly);


                }
            }
        }
#endif

#if !NOLOADASSEMBLY

        /// <summary>
        /// Loads "auto modules" defined in the loaded assemblies.
        /// </summary>
        public virtual void UpdateAutoLoadModuleIfNeed()
        {
            if (!Injector.Settings.IsAutoLoadModuleEnabled) return;
            LoadRequiredModule();
            Type attributeType = Injector.Settings.AttributeForAutoLoadModule;
            lock (StaticLocker)
            {
                var types = new IList<Type>[CachedAssembly.Values.Count];
                CachedAssembly.Values.CopyTo(types, 0);
                LoadModules(types, attributeType);
            }
        }

        /// <summary>
        /// Gets all the <see cref="Assembly"/> for loads.
        /// </summary>
        /// <returns></returns>
        protected virtual IList<Assembly> GetAssemblies()
        {
#if DYNAMICASSEMBLY
            var getAssembliesMethod = typeof (AppDomain).GetMethod("GetAssemblies",
                                         BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.InvokeMethod |
                                         BindingFlags.Instance);
            return (IList<Assembly>) ExpressionReflectionAccessProvider
                                         .GetInvokeMethodDelegate<InvokeMethodDelegate<AppDomain>, AppDomain>(
                                             getAssembliesMethod, false, true)(AppDomain.CurrentDomain);
#else
            return AppDomain.CurrentDomain.GetAssemblies();
#endif
        }

        /// <summary>
        /// Loads required "auto modules" defined in the loaded assemblies.
        /// </summary>
        protected void LoadRequiredModule()
        {
            if (!Injector.Settings.IsAutoLoadModuleEnabled) return;
            lock (StaticLocker)
            {
                var types = new IList<Type>[CachedAssembly.Values.Count];
                CachedAssembly.Values.CopyTo(types, 0);
                LoadModules(types, typeof(AutoLoadModuleRequiredAttribute));
            }
        }

        /// <summary>
        /// Loads modules Loads modules into an <see cref="IInjector"/>. If the modules are loaded skips them.
        /// </summary>
        /// <param name="types"></param>
        /// <param name="attributeType"></param>
        protected void LoadModules(IList<IList<Type>> types, Type attributeType)
        {
            for (int index = 0; index < types.Count; index++)
            {
                IList<Type> modules = types[index];
                if (modules.Count == 0) continue;
                foreach (Type moduleType in modules)
                {
                    lock (_loadedModule)
                    {
                        if (_loadedModule.Contains(moduleType) || !moduleType.IsDefined(attributeType, true) ||
                            Injector.ModuleIsLoaded(moduleType))
                            continue;
                        Injector.Load(moduleType);
                        _loadedModule.Add(moduleType);
                    }
                }
            }
        }

        private void SettingsChanged(string arg1, object arg2)
        {
            if (!arg1.Equals("IsAutoLoadModuleEnabled") && !arg1.Equals("AttributeForAutoLoadModule")) return;
            UpdateAutoLoadModuleIfNeed();
        }
#endif

        private void LoadModulesInternal(IList<Assembly> assemblies)
        {
            lock (StaticLocker)
            {
                for (int i = 0; i < assemblies.Count; i++)
                {
                    Assembly assembly = assemblies[i];
                    foreach (Type exportType in ExportTypes(assembly))
                    {
                        if (Injector.ModuleIsLoaded(exportType)) continue;
                        Injector.Load(exportType);
                    }
                }
            }
        }

#if SCANASSEMBLY
        protected static IList<Assembly> GetAssembliesInDirectoryWithExtension(string path, string extension,
                                                                               bool throwError,
                                                                               IList<string> assembliesToSkip)
        {
            var result = new List<Assembly>();
            foreach (FileInfo file in new DirectoryInfo(path).GetFiles(extension, SearchOption.AllDirectories))
            {
                try
                {
                    bool skip = false;
                    foreach (string s in assembliesToSkip)
                    {
                        if (s.Equals(file.Name, StringComparison.CurrentCultureIgnoreCase))
                        {
                            skip = true;
                            break;
                        }
                    }
                    if (skip)
                        continue;

                    result.Add(Assembly.LoadFrom(file.FullName));
                }
                catch (BadImageFormatException bif)
                {
                    if (throwError)
                        throw new InvalidOperationException(string.Format("Could not load {0}.", file.FullName), bif);
                }
                catch (Exception)
                {
                    if (throwError)
                        throw;
                }
            }

            return result;
        }
#endif

        #endregion

        #region Overrides of Component

        /// <summary>
        /// Indicates whether the component is unique.
        /// </summary>
        public override sealed bool IsSingle
        {
            get { return true; }
        }

        /// <summary>
        /// Returns the component type. Need to find a component in the container.
        /// </summary>
        public override sealed Type ComponentType
        {
            get { return typeof(IModuleManagerComponent); }
        }

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// A new object that is a copy of this instance.
        /// </returns>
        public override IComponent Clone()
        {
            var moduleManagerComponent = new ModuleManagerComponent();
#if SCANASSEMBLY
            foreach (string excludedFile in ExcludedFiles)
            {
                moduleManagerComponent.ExcludedFiles.Add(excludedFile);
            }
            foreach (string extension in Extensions)
            {
                if (moduleManagerComponent.Extensions.Contains(extension)) continue;
                moduleManagerComponent.Extensions.Add(extension);
            }
            moduleManagerComponent.AutoScanAssembly = AutoScanAssembly;
#endif
            return moduleManagerComponent;
        }

        /// <summary>
        /// Occurs on load component.
        /// </summary>
        protected override void OnLoad()
        {
            lock (StaticLocker)
            {
#if SCANASSEMBLY
                if (AutoScanAssembly && !_isFirstScanAssembly)
                {
                    RescanFiles();
                    _isFirstScanAssembly = true;
                }
#endif
#if !NOLOADASSEMBLY
                if (!_isFirstLoadAssembly)
                {
                    foreach (var assembly in GetAssemblies())
                        ExportTypes(assembly);
                    _isFirstLoadAssembly = true;
                }                
#endif
            }
#if !NOLOADASSEMBLY
            UpdateAutoLoadModuleIfNeed();
            Injector.Settings.SettingChanged += SettingsChanged;
#endif
        }

#if !NOLOADASSEMBLY
        /// <summary>
        /// Occurs on unload component.
        /// </summary>
        protected override void OnUnload()
        {
            Injector.Settings.SettingChanged -= SettingsChanged;
        }
#endif

        #endregion

        #region Static method

        protected IList<Type> ExportTypes(Assembly assembly)
        {
            IList<Type> listTypes;
            if (!CachedAssembly.TryGetValue(assembly, out listTypes))
            {
                try
                {
                    listTypes = new List<Type>();
                    foreach (Type exportedType in assembly.GetTypes())
                    {
#if NETFX_CORE
                        if (!typeof(IInjectorModule).IsAssignableFrom(exportedType) || exportedType.GetTypeInfo().IsAbstract)
#else
                        if (!typeof(IInjectorModule).IsAssignableFrom(exportedType) || exportedType.IsAbstract)
#endif
                            continue;
                        listTypes.Add(exportedType);
                    }
                    CachedAssembly.Add(assembly, listTypes);
                }
                catch (ReflectionTypeLoadException)
                {
                }
            }
            return listTypes;
        }

        #endregion
    }
}